/*
 * Copyright (C) 2012 Damien Girard <dgirard@nativesoft.fr>
 * Copyright (C) 2012 Sebastien Baudrand <sebbaudrand@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package fr.umlv.qroxy.rules;

import fr.umlv.qroxy.http.*;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.*;
import java.util.Map.Entry;
import java.util.logging.Logger;

/**
 * HTTP Header.
 *
 * Parses a ByteBuffer and generate an usable HTTP header.
 *
 * @author Damien Girard <dgirard@nativesoft.fr>
 * @author Sebastien BAUDRAND <sebbaudrand@gmail.com>
 */
public class HeaderHttp {

    private static final Logger logger = Logger.getLogger(HeaderHttp.class.getName());
    private static final Charset httpHeaderCharset = Charset.forName("ISO-8859-1");
    /*
     * Structural fields.
     */
    private final Map<String, String> properties = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER); // For ignoring case.
    /*
     * HTTP Header cached fields.
     */
    private final String httpHeaderFirstLine;
    private final String urlToCall;
    //private final URI uri;
    private final HttpMethods httpMethod;
    private final int httpCode;
    private final int endOfHeaderInBuffer;
    // Thoses fields are not final for easier programming.
    private HttpCloseRules closeRule = null; // Must not be null after constructor!
    private HttpTransferMethods transferMethod = null; // Must not be null after constructor!
    private int contentLength;
    private int httpVersionMinor;
    private int httpVersionMajor;

    /**
     * Parse the buffer in order to create an usable Http header.
     *
     * @param buffer Buffer to parse.
     * @param endOfHeaderInBuffer Position where the header stop in the buffer.
     *
     * @throws HttpException HTTP error when parsing the header.
     */
    public HeaderHttp(ByteBuffer buffer, int endOfHeaderInBuffer) throws HttpException, CharacterCodingException {
        this.endOfHeaderInBuffer = endOfHeaderInBuffer;

        // Parsing the header.
        CharsetDecoder decoder = httpHeaderCharset.newDecoder();

        Scanner sc = new Scanner(decoder.decode(buffer));

        // Reading the command.
        httpHeaderFirstLine = sc.nextLine();
        StringTokenizer tokenizer = new StringTokenizer(httpHeaderFirstLine);

        try {
            String methodToken = tokenizer.nextToken();
            if (methodToken.startsWith("HTTP")) {
                // Special handle of HTTP method.
                // Format: HTTP/1.0 302 Found
                readHttpVersion(methodToken);

                // Getting the HTTP code.
                httpCode = Integer.parseInt(tokenizer.nextToken());

                // Filling this header.
                httpMethod = HttpMethods.HTTP;
                //uri = null;
                urlToCall = null;
            } else {
                httpMethod = Enum.valueOf(HttpMethods.class, methodToken); // Qroxy support only RFC 1945.

                // Getting url.
                String pathToken = tokenizer.nextToken();
                urlToCall = pathToken;
                //uri = new URI(urlToCall);

                // Getting the HTTP Version.
                String httpVerToken = tokenizer.nextToken();
                readHttpVersion(httpVerToken);

                // Filling this header.
                httpCode = 0;
            }
            //} catch (URISyntaxException ex) {
            //   throw new HttpException(HttpCodes.HTTP_BAD_METHOD, "Cannot parse the uri " + ex);
        } catch (IllegalArgumentException ex) {
            throw new HttpException(HttpCodes.HTTP_BAD_METHOD, "Unknown method " + ex);
        } catch (NoSuchElementException ex) {
            throw new HttpException(HttpCodes.HTTP_BAD_REQUEST, "Cannot get command " + ex);
        }

        // Command read, parsing the other commands.
        String line;
        while (sc.hasNextLine()) {
            line = sc.nextLine();

            // Handling the empty line. (End of the HTTP header)
            if (line.isEmpty()) {
                break;
            }

            StringTokenizer request = new StringTokenizer(line, ":");
            try {
                String command = request.nextToken();
                StringBuilder value = new StringBuilder();
                while (request.hasMoreElements()) {
                    value.append(request.nextToken(""));
                }
                value.deleteCharAt(0); // Removing the trailing ":"

                properties.put(command, value.toString().trim());
            } catch (NoSuchElementException ex) {
                throw new HttpException(HttpCodes.HTTP_BAD_REQUEST, "Invalid request " + ex);
            }
        }

        // Handling specific fields.

        if (this.httpMethod == HttpMethods.GET || this.httpMethod == HttpMethods.HEAD) {
            this.contentLength = 0;
            this.transferMethod = HttpTransferMethods.FIXED;
        } else {
            // Chunked connection.
            String transferEncoding = properties.get("Transfer-Encoding");
            if (transferEncoding != null) {
                if (transferEncoding.equals("chunked")) {
                    this.contentLength = -1;
                    this.transferMethod = HttpTransferMethods.CHUNKED;
                } else {
                    throw new HttpException(HttpCodes.HTTP_UNIMPLEMENTED, "Unknown Transfer-Encoding. " + transferEncoding);
                }
            } else {
                // Not chunked, so getting the content length.
                this.transferMethod = HttpTransferMethods.FIXED;
                String contLength = properties.get("Content-Length");
                if (contLength != null) {
                    try {
                        contentLength = Integer.parseInt(contLength);
                    } catch (NumberFormatException ex) {
                        throw new HttpException(HttpCodes.LENGTH_REQUIRED, "Content-length not readable. " + ex);
                    }
                } else {
                    this.contentLength = 0; // No data.
                    //throw new HttpException(HttpCodes.LENGTH_REQUIRED, "No length given and no chunked connection.");
                }
            }
        }

        // Connection type.
        String connectionClose = properties.get("Connection");
        if (connectionClose != null) {
            if (connectionClose.equalsIgnoreCase("close")) {
                this.closeRule = HttpCloseRules.CONNECTION_CLOSE_AFTER;
            } else if (connectionClose.equalsIgnoreCase("Keep-Alive")) {
                this.closeRule = HttpCloseRules.KEEPALIVE_TIMEOUT;
            } else {
                // Unsuportted HTTP header.
                logger.fine("Unsuported 'Connection' field value: " + connectionClose);
            }
        }
        if (closeRule == null) {
            // Keepalive ?
            // HTTP 1.0: No, HTTP > 1.1: Yes
            if (this.httpVersionMajor >= 1 && this.httpVersionMinor >= 1) {
                this.closeRule = HttpCloseRules.KEEPALIVE_TIMEOUT;
            } else {
                this.closeRule = HttpCloseRules.CONNECTION_CLOSE_AFTER;
            }
        }

        // Doing some checks.

        // No content length is forbidden in HTTP 1.0 (Todo: Check that for HTTP 0.9 ?)
        if (contentLength == -1) {
            if ((this.httpVersionMajor == 1 && this.httpVersionMinor == 0)
                    || (this.httpVersionMajor < 1)) {
                throw new HttpException(HttpCodes.LENGTH_REQUIRED, "Content-length not defined.");
            }
        }

        // If "HTTP:" is given in the header, I do not use the host information.
        if (urlToCall != null && urlToCall.toLowerCase().startsWith("http:")) {
            StringTokenizer parseHost = new StringTokenizer(urlToCall);
            parseHost.nextToken("/");
            String host = parseHost.nextToken("/");
            if (host != null) {
                properties.put("host", host);
            }
        }

        // Some assertion check.
        if (this.closeRule == null) {
            throw new AssertionError(closeRule);
        }
        if (this.transferMethod == null) {
            throw new AssertionError(transferMethod);
        }

        // HTTP Header parsed.
    }

    /**
     * Helpers that read the HTTP version.
     *
     * @param httpVerToken HTTP/X.X string.
     * @throws HttpException If the http version is not supported.
     */
    private void readHttpVersion(String httpVerToken) throws HttpException {
        StringTokenizer versionTokenizer = new StringTokenizer(httpVerToken, "/");
        versionTokenizer.nextToken();
        httpVersionMinor = Integer.parseInt(versionTokenizer.nextToken("/."));
        httpVersionMajor = Integer.parseInt(versionTokenizer.nextToken());
        if (httpVersionMajor >= HttpConstants.HTTP_MAJOR_VERSION_SUPPORT) {
            if (httpVersionMinor > HttpConstants.HTTP_MINOR_VERSION_SUPPORT) {
                throw new HttpException(HttpCodes.HTTP_VERSION, "Unsuported version " + httpVerToken);
            }
        }
    }

    /**
     * Get the properties initialized with the constructor.
     *
     * @return The property.
     */
    public Map<String, String> getProperties() {
        return this.properties;
    }

    /**
     * Returns the HTTP method.
     *
     * @return The HTTP method.
     */
    public HttpMethods getHttpMethod() {
        return httpMethod;
    }

    /**
     * Returns the url to call on the server.
     *
     * @return The Url.
     */
    public String getUrlToCall() {
        return urlToCall;
    }

    /**
     * Returns the uri to call.
     *
     * @return the uri object.
     */
    //public URI getUri() {
    //    return uri;
    //}
    /**
     * Gets the content-length.
     *
     * @return The content length value of this http header. -1 means that datas
     * are in chunked mode.
     */
    public int getContentLength() {
        return contentLength;
    }

    /**
     * Gets the http code. (eg. 404)
     */
    public int getHttpCode() {
        return httpCode;
    }

    /**
     * Gets the HTTP close rule.
     */
    public HttpCloseRules getCloseRule() {
        return closeRule;
    }

    /**
     * Gets the position where the header ends in the buffer.
     */
    public int getEndOfHeaderInBuffer() {
        return endOfHeaderInBuffer;
    }

    /**
     * Gets the transfer method.
     */
    public HttpTransferMethods getTransferMethod() {
        return transferMethod;
    }

    /**
     * Set the content length.
     *
     * Usefull for chunked connection.
     */
    public void setContentLength(int contentLength) {
        this.contentLength = contentLength;
    }

    /**
     * Writes this Http header to the buffer.
     * 
     * @param buffer The buffer to append the data.
     * 
     * @throws BufferOverflowException The buffer is not large enough to hold the HTTP header. (Clear it before calling this method!)
     */
    public void writeHeaderHttpToBuffer(ByteBuffer buffer) throws BufferOverflowException, CharacterCodingException {
        // Using the right encoding.
        CharsetEncoder encoder = httpHeaderCharset.newEncoder();

        // Gooooo.
        buffer.put(encoder.encode(CharBuffer.wrap(httpHeaderFirstLine + "\r\n")));

        for (Entry<String, String> e : properties.entrySet()) {
            buffer.put(encoder.encode(CharBuffer.wrap(e.getKey() + ": " + e.getValue() + "\r\n")));
        }

        // Writing the final CRLF.
        buffer.put((byte) '\r');
        buffer.put((byte) '\n');

        // Buffer wrote.
    }

    /**
     * Returns the position of the end of the http buffer.
     *
     * @param buffer The buffer to check.
     *
     * @return -1 if the buffer does not contains an http header.
     */
    public static int bufferHaveHttpHeader(ByteBuffer buffer) throws CharacterCodingException {
        // Looking for the end of the header. 
        // 3 kind of end : ("\r\n\r\n") or "\r\r" or "\n\n"
        int count = 0;
        byte lastChar = 0;
        while (buffer.hasRemaining()) {
            byte b = buffer.get();
            //System.out.print((char) b);
            if (b == (byte) '\r' || b == (byte) '\n') {
                if (b == lastChar && count == 1) // \r\r or \n\n
                {
                    return buffer.position();
                }
                count++;
                if (count >= 4) // \r\n\r\n
                {
                    return buffer.position();
                }
            } else {
                count = 0;
            }

            lastChar = b;
        }

        return -1;
    }
}
